package cn.hutool.poi.excel;

import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.cell.CellUtil;
import cn.hutool.poi.excel.cell.data.SheetData;
import cn.hutool.poi.excel.cell.data.TableData;
import cn.hutool.poi.excel.cell.data.WorkBookData;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellCopyPolicy;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 使用Excel模版的写入器<br>
 * 此工具用于基于指定Excel模版文件的样式，将POI数据填充到Excel模版文件，并输出成新的Excel文件或者输出流，此对象可完成以下两个功能
 *
 * <pre>
 * 1. 填充模版Excel的多个动态数据表格区域
 * 2. 通过变量占位标识符“{变量名}”,填写一些相对固定位置的单元格的值
 * 3. 根据指定的Excel模版的指定的sheet，复制并填充成多个Sheet
 * </pre>
 *
 * <br>
 * 为什么要做一个这样的工具。因为实际的POI操作再怎么写，也无法实现Office的Excel本身的一些特效，或者说很难实现其中的一些效果。举几个例子
 * <pre>
 * 1、带标题行过滤的报表
 * 2、带冻结行列的报表
 * 3、插入了公司Logo或者其他资源的报表
 * </pre>
 *
 * @author 徐明龙 XuMingLong 2021-11-16
 */
public class ExcelTemplateWriter {

	/**
	 * 屏蔽实例的创建
	 * @author 徐明龙 XuMingLong 2021-11-16
	 */
	private ExcelTemplateWriter() {
		throw new IllegalStateException("工具类，不需要构建实例对象。");
	}

	/**
	 * 占位标识符“{变量名}”的匹配模式
	 * @author 徐明龙 XuMingLong 2021-11-16
	 */
	private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);

	/**
	 * 行复制规则
	 * @author 徐明龙 XuMingLong 2021-11-16
	 */
	private static final CellCopyPolicy DEFAULT_ROW_COPY_POLICY = new CellCopyPolicy.Builder().build();

	/**
	 * 使用替换模版内容的方式导出Excel,每个SheetData根据下标顺序的填充每个模版Sheet。
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param workBookData Excel工作簿数据对象
	 * @param templateFileInputStream 模版文件的输入流
	 * @param out 导出的输出流
	 * @param isCloseOut 是否关闭输出流
	 * @return boolean 返回导出是否成功
	 */
	public static boolean exportWithReplaceTemplate(WorkBookData workBookData, InputStream templateFileInputStream,
		OutputStream out,boolean isCloseOut) {
		XSSFWorkbook workbook = WorkbookUtil.createXSSFBook(templateFileInputStream);
		List<SheetData> sheetDataList =  workBookData.getSheetDataList();
		for(int i=0,size = sheetDataList.size(); i<size;i++){
			SheetData sheetData = sheetDataList.get(i);
			//获取模版的Sheet
			XSSFSheet templateSheet = workbook.getSheetAt(i);
			if(templateSheet==null){
				//模版和SheetData不匹配，返回false
				return false;
			}
			//写Sheet数据
			writeSheetData(templateSheet,sheetData,workBookData.getFixedDataMap());
			//更新Sheet名称
			if(StrUtil.isNotBlank(sheetData.getSheetName())){
				workbook.setSheetName(i,sheetData.getSheetName());
			}
		}
		flush(workbook, out, isCloseOut);
		return true;
	}

	/**
	 * 将Excel Workbook刷出到输出流
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param workbook 工作簿
	 * @param out 输出流
	 * @param isCloseOut 是否关闭输出流
	 * @throws IORuntimeException IO异常
	 * @return void
	 */
	private static void flush(XSSFWorkbook workbook,OutputStream out,boolean isCloseOut) throws IORuntimeException{
		try {
			workbook.write(out);
			out.flush();
		} catch (IOException e) {
			throw new IORuntimeException(e);
		} finally {
			if (isCloseOut) {
				IoUtil.close(out);
			}
		}
	}

	/**
	 * 写Sheet数据
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param sheet  要操作的Sheet
	 * @param sheetData sheet数据
	 * @param fixedDataMap 工作簿级别的固定占位符映射
	 * @return void
	 */
	private static void writeSheetData(XSSFSheet sheet, SheetData sheetData, Map<String, String> fixedDataMap) {
		List<TableData> tableDataList = sheetData.getTableDataList();
		//先写固定的值
		Map<String, String> margeFixedDataMap = null;
		//合并Map
		if(MapUtils.isNotEmpty(fixedDataMap)) {
			margeFixedDataMap = fixedDataMap;
			if(MapUtils.isNotEmpty(sheetData.getFixedDataMap())){
				mergerMap(margeFixedDataMap,sheetData.getFixedDataMap(),(v1,v2)->v2);
			}
		}else{
			if(MapUtils.isNotEmpty(sheetData.getFixedDataMap())){
				margeFixedDataMap = sheetData.getFixedDataMap();
			}
		}
		//固定值变量替换
		writeFixedData(sheet,margeFixedDataMap);

		//循环处理一个Sheet有多个子表
		if(CollectionUtils.isNotEmpty(tableDataList)) {
			int rowOffset = 0;
			for(TableData tableData:tableDataList){
				writeTableData(sheet,tableData,rowOffset);
				rowOffset += tableData.getData().size()-1;
			}
		}
	}

	/**
	 * 合并Map，将Map2的数据合并到Map1
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param map1 主Map
	 * @param map2 次Map
	 * @param mergeFunction 合并方法
	 * @return void
	 */
	private static <K, V> void  mergerMap(Map<K, V> map1, Map<K, V> map2, BinaryOperator<V> mergeFunction) {
		for (Map.Entry<K,V> e : map2.entrySet()){
			map1.merge(e.getKey(), e.getValue(), mergeFunction);
		}
	}


	/**
	 * 写表格数据
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param sheet 要操作的Sheet
	 * @param tableData 表格数据对象
	 * @param rowOffset 起始行的偏移值
	 * @return void
	 */
	private static void writeTableData(XSSFSheet sheet, TableData tableData,int rowOffset) {
		List<List<?>> data = tableData.getData();
		if(CollectionUtils.isNotEmpty(data)) {
			int beginY = tableData.getBeginY() + rowOffset;
			Row beginRow = sheet.getRow(beginY);
			//待添加的行数
			int pendingAddRows = data.size();
			//开始移动的行号
			int beginMoveRowNum = beginY+1;
			//如果移动的开始行不存在，则创建一个空行，否则复制的时候会丢掉不存在的行
			if(sheet.getRow(beginMoveRowNum)==null){
				sheet.createRow(beginMoveRowNum);
			}
			//当前Sheet的最后的行号
			int lastRowNum = sheet.getLastRowNum();
			//计算移动的行数
			int movedRows = lastRowNum-beginMoveRowNum+1;
			//临时区的开始行号
			int beginTempRowNum = lastRowNum+pendingAddRows+1;
			//临时区的结束行号
			int lastTempRowNum = beginTempRowNum+movedRows-1;
			//末尾部分的最终开始行号
			int beginFinallyRowNum = pendingAddRows+beginY;

			//移动开始行以后的行到临时的开始行
			moveRows(sheet,beginMoveRowNum,lastRowNum,beginTempRowNum);
			//在空出位置插入新行并复制第一行的样式
			for (int i = beginMoveRowNum; i <beginFinallyRowNum; i++){
				Row row = sheet.getRow(i);
				if (row != null){
					sheet.removeRow(row);
				}
				//创建空白的行
				sheet.createRow(i);
				if(beginRow!=null){
					//复制第一行的样式到当前行
					sheet.copyRows(beginY,beginY, i, DEFAULT_ROW_COPY_POLICY);
				}
			}
			//将移动的行从临时开始行回移新增好的行的末尾
            /*
              为什么不直接第一次移动的预期的末尾呢？是因为POX只支持Copy行的操作，而copy操作不是完整的区间覆盖。
              如果源区间内带有格式，并且目的区间和源区间有重叠话，会导致无法copy后源区间和目的区间混合在一起，无法清理。
              因此实现移动行区间的操作时，需要先将源行区间复制到一个临时的区域，然后清理掉源行区间的行和样式，再插入需要新增的行
              ，最后再将被移动的行区间移动回新的行末尾。
             */
			moveRows(sheet,beginTempRowNum,lastTempRowNum,beginFinallyRowNum);
			//写表格数据
			writeData(sheet,data,beginY,tableData.getBeginX());
		}
	}


	/**
	 * 写二维表数据
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param sheet 要操作的Sheet
	 * @param data 二维表数据
	 * @param beginY 开始的行号
	 * @param beginX 开始的列号
	 * @return void
	 */
	private static void writeData(XSSFSheet sheet, List<List<?>> data, int beginY, int beginX) {
		//填充数据
		int rowNum = beginY;
		for(List<?> rowList:data) {
			Row row = sheet.getRow(rowNum);
			if (row == null){
				row = sheet.createRow(rowNum);
			}
			rowNum++;
			for(int x=0;x<rowList.size();x++) {
				Object cellData=rowList.get(x);
				if(cellData==null) {
					continue;
				}
				int cellIndex = beginX+x;
				Cell cell = row.getCell(cellIndex);
				if(cell==null) {
					cell = row.createCell(cellIndex);
				}
				CellUtil.setCellValue(cell, cellData,null,false);
			}
		}
	}

	/**
	 * 移动行
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param sheet 要操作的Sheet
	 * @param srcBeginRowNum 待移动的源开始行号
	 * @param srcEndRowNum 待移动的源结束行号
	 * @param beginDestRowNum  移动的目的开始行号
	 * @return void
	 */
	private static void moveRows(XSSFSheet sheet, int srcBeginRowNum, int srcEndRowNum, int beginDestRowNum) {
		//先复制到目的位置
		sheet.copyRows(srcBeginRowNum, srcEndRowNum, beginDestRowNum, DEFAULT_ROW_COPY_POLICY);
		//清理之前残留的合并单元格
		List<Integer> removeMergedRegion = new ArrayList<>();
		for(int i=sheet.getMergedRegions().size()-1;i>=0;i--){
			CellRangeAddress address = sheet.getMergedRegions().get(i);
			//如果合并单元格在复制前的位置，则删除
			if(address.getFirstRow() >= srcBeginRowNum && address.getFirstRow()<beginDestRowNum ){
				removeMergedRegion.add(i);
			}
		}
		//执行清理合并的单元格
		for(Integer i:removeMergedRegion){
			sheet.removeMergedRegion(i);
		}
		//删除移动后原地的行
		for (int i = srcBeginRowNum; i <= srcEndRowNum; i++){
			Row row = sheet.getRow(i);
			if (row != null){
				//删除复制后残留的行
				sheet.removeRow(row);
			}
		}

	}


	/**
	 * 替换固定的占位符的值
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param sheet 要操作的Sheet
	 * @param fixedDataMap 占位符的值的映射
	 * @return void
	 */
	private static void writeFixedData(XSSFSheet sheet, Map<String, String> fixedDataMap) {
		if(MapUtils.isEmpty(fixedDataMap)) {
			return ;
		}
		int lastRowNo = sheet.getLastRowNum();
		for(int i= 0;i<=lastRowNo;i++ ) {
			XSSFRow row = sheet.getRow(i);
			if(row==null) {
				continue;
			}
			Iterator<Cell> cellIterator = row.cellIterator();
			while(cellIterator.hasNext()) {
				Cell cell= cellIterator.next();
				if(cell==null) {
					continue;
				}
				if(!cell.getCellType().equals(CellType.STRING)) {
					continue;
				}
				String cellValue = StrUtil.trimToEmpty(cell.getStringCellValue());
				if(StrUtil.isBlank(cellValue)) {
					continue;
				}
				Matcher matcher = matcher(cellValue);
				//替换文本域内容
				if (matcher.find()) {
					do {
						String key = matcher.group(1);
						String value = fixedDataMap.getOrDefault(key,StrUtil.EMPTY);
						cellValue = matcher.replaceFirst(value);
					}
					while ((matcher = matcher(cellValue)).find()) ;
					cell.setCellValue(cellValue);
				}

			}
		}

	}

	/**
	 * 正则匹配占位符的字符串
	 * @author 徐明龙 XuMingLong 2021-11-16
	 * @param str 待搜索的字符串
	 * @return java.util.regex.Matcher 匹配的结果
	 */
	private static Matcher matcher(String str) {
		return VARIABLE_PATTERN.matcher(str);
	}

}
